Feat: 입력 필드 상태 변화 수정 - #116
Conversation
📝 WalkthroughWalkthroughCredit purchases now redirect through a backend checkout URL and verify results by polling payment status. Login and signup forms add touched-state tracking to control validation feedback. ChangesCredit payment flow
Login form validation
Estimated code review effort: 3 (Moderate) | ~30 minutes Sequence Diagram(s)sequenceDiagram
participant CreditCard
participant Backend
participant CreditContent
participant PaymentAPI
CreditCard->>Backend: preparePurchase(planCode)
Backend-->>CreditCard: checkoutPage URL
CreditCard->>Backend: redirect to checkoutPage
CreditContent->>PaymentAPI: poll orderId
PaymentAPI-->>CreditContent: payment status
CreditContent->>CreditContent: handle COMPLETED or FAILED
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (5)
jobdri/src/app/credit/page.tsx (3)
55-55: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueReplace blocking
alert()with the existingToastcomponent used elsewhere in this flow (jobdri/src/components/common/cards/CreditCard.tsx), for consistent UX.Also applies to: 61-61, 73-73
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@jobdri/src/app/credit/page.tsx` at line 55, Replace the blocking alert calls in the credit page completion flows with the existing Toast component pattern used by CreditCard.tsx. Update all affected success messages, including the occurrences corresponding to lines 55, 61, and 73, while preserving their current message text and triggering behavior.
77-81: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueThe
Promise.resolve()wrapper is unnecessary. CallingsetIsConfirming(true)synchronously inside an effect is standard and does not trigger a cascading-render warning; the microtask defer only makes the flow harder to follow (and leaves the initialpollStatusun-tracked by the cleanup path).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@jobdri/src/app/credit/page.tsx` around lines 77 - 81, Remove the Promise.resolve microtask wrapper around setIsConfirming and pollStatus in the effect. Invoke setIsConfirming(true) synchronously, then start pollStatus through the existing cleanup-tracked flow so the initial poll is handled consistently.
26-27: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
routeris initialized but the effect mutates history directly.window.history.replaceStatebypasses the App Router, and theCOMPLETEDpath then does a fullwindow.location.reload(). Preferrouter.replace(window.location.pathname)plusrouter.refresh()(or refetching plans/credits) to clearorderIdand update data without a hard reload.Also applies to: 53-56
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@jobdri/src/app/credit/page.tsx` around lines 26 - 27, Update the effect using the initialized router in the credit page: replace direct window.history.replaceState calls with router.replace(window.location.pathname), and replace the COMPLETED branch’s window.location.reload() with router.refresh() (or the existing plans/credits refetch). Preserve clearing orderId while updating App Router data without a hard reload.jobdri/src/components/common/cards/CreditCard.tsx (1)
73-78: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value
isLoadingstaystruewhen the redirect is slow, but that's the only state left set. Success intentionally keeps the spinner until navigation; fine. Consider validating the redirect target scheme beforewindow.location.assignso a compromised/incorrect backend value can't produce ajavascript:navigation.🛡️ Optional hardening
- if (checkoutPage) { + const target = checkoutPage ? new URL(checkoutPage, window.location.origin) : null; + if (target && (target.protocol === "https:" || target.protocol === "http:")) { // 백엔드에서 생성한 토스페이 결제 페이지로 이동 - window.location.assign(checkoutPage); + window.location.assign(target.toString()); } else {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@jobdri/src/components/common/cards/CreditCard.tsx` around lines 73 - 78, Validate the redirect URL immediately before the window.location.assign call in the payment flow, allowing only trusted http/https schemes and rejecting javascript: or other unsafe schemes. On rejection, follow the existing payment-error cleanup path, while preserving the success behavior that keeps isLoading true until navigation.jobdri/src/lib/api/credit.ts (1)
108-122: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRemove the commented-out
confirmPurchaseblock.Dead code; git history preserves it.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@jobdri/src/lib/api/credit.ts` around lines 108 - 122, Remove the commented-out confirmPurchase function block from the payment API module, leaving the surrounding code unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@jobdri/src/app/credit/page.tsx`:
- Around line 38-88: Update the polling flow in the useEffect around pollStatus
to track and clear the active timeout during cleanup, and re-check the
cancellation flag after each awaited checkPaymentStatus call before performing
state updates, alerts, history changes, or reloads. Add a finite polling limit
for PENDING, PROCESSING, and unknown statuses; when the limit is reached, stop
polling, remove the URL order state, clear confirmation, and report that payment
status could not be confirmed. Ensure all terminal and error paths clear any
scheduled timeout.
In `@jobdri/src/components/login/EmailLoginScreen.tsx`:
- Line 323: Make touched-state resets field-specific in handleInputChange and
handlePasswordChange: ensure email input changes reset only emailTouched,
password input changes reset passwordTouched, and confirmation input changes
reset passwordConfirmTouched. Pass the appropriate setter through the shared
helper or perform the resets in each dedicated handler, without clearing
unrelated fields.
- Around line 774-780: Wire blur handling for both signup fields: add an onBlur
handler to the signup email input that marks the email as touched, and restore
the commented password-confirmation onBlur handler so it marks that field as
touched. Update the signup form inputs near the signupErrorMessage logic and
preserve the existing validation/error conditions.
In `@jobdri/src/lib/api/credit.ts`:
- Around line 124-129: Update jobdri/src/lib/api/credit.ts lines 124-129 by
giving checkPaymentStatus an explicit response type and returning the unwrapped
result; extend PreparePaymentResult with checkoutPage. In
jobdri/src/components/common/cards/CreditCard.tsx lines 57-72, remove the cast
and fallback, and read response.checkoutPage directly. In
jobdri/src/app/credit/page.tsx lines 45-49, remove the cast and read
response.status directly.
- Around line 124-129: Update checkPaymentStatus to follow the file’s
established API request pattern: build the URL with API_BASE_URL, include
getAuthHeaders(), and pass the response through checkResponse so unauthorized
responses invoke handleUnauthorized(). Unwrap the response’s result field and
declare the returned payment-status type, allowing callers to use it without
casts.
---
Nitpick comments:
In `@jobdri/src/app/credit/page.tsx`:
- Line 55: Replace the blocking alert calls in the credit page completion flows
with the existing Toast component pattern used by CreditCard.tsx. Update all
affected success messages, including the occurrences corresponding to lines 55,
61, and 73, while preserving their current message text and triggering behavior.
- Around line 77-81: Remove the Promise.resolve microtask wrapper around
setIsConfirming and pollStatus in the effect. Invoke setIsConfirming(true)
synchronously, then start pollStatus through the existing cleanup-tracked flow
so the initial poll is handled consistently.
- Around line 26-27: Update the effect using the initialized router in the
credit page: replace direct window.history.replaceState calls with
router.replace(window.location.pathname), and replace the COMPLETED branch’s
window.location.reload() with router.refresh() (or the existing plans/credits
refetch). Preserve clearing orderId while updating App Router data without a
hard reload.
In `@jobdri/src/components/common/cards/CreditCard.tsx`:
- Around line 73-78: Validate the redirect URL immediately before the
window.location.assign call in the payment flow, allowing only trusted
http/https schemes and rejecting javascript: or other unsafe schemes. On
rejection, follow the existing payment-error cleanup path, while preserving the
success behavior that keeps isLoading true until navigation.
In `@jobdri/src/lib/api/credit.ts`:
- Around line 108-122: Remove the commented-out confirmPurchase function block
from the payment API module, leaving the surrounding code unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: dcaa9eae-ac19-40f7-8ccf-bf41a13a5cbd
📒 Files selected for processing (4)
jobdri/src/app/credit/page.tsxjobdri/src/components/common/cards/CreditCard.tsxjobdri/src/components/login/EmailLoginScreen.tsxjobdri/src/lib/api/credit.ts
| if (orderId) { | ||
| let isPolling = true; | ||
|
|
||
| const pollStatus = async () => { | ||
| try { | ||
| await confirmPurchase(paymentKey, orderId, Number(amount)); | ||
| window.history.replaceState(null, "", window.location.pathname); | ||
| setIsConfirming(false); | ||
| setTimeout(() => { | ||
| const response = await checkPaymentStatus(orderId); | ||
| // API 타입이 지정되지 않은 경우를 대비한 타입 단언 | ||
| const data = response as unknown as { | ||
| status?: string; | ||
| result?: { status?: string }; | ||
| }; | ||
| const status = data.status || data.result?.status; | ||
|
|
||
| if (status === "COMPLETED") { | ||
| isPolling = false; | ||
| window.history.replaceState(null, "", window.location.pathname); | ||
| setIsConfirming(false); | ||
| alert("크레딧 충전이 완료되었습니다!"); | ||
| window.location.reload(); | ||
| }, 100); | ||
| } catch (error) { | ||
| console.error("결제 승인 실패:", error); | ||
| setIsConfirming(false); | ||
| alert("결제 승인에 실패했습니다. 다시 시도해 주세요."); | ||
| // 실패 시에도 쿼리 파라미터를 날려 중복 요청 방지 | ||
| } else if (status === "FAILED") { | ||
| isPolling = false; | ||
| window.history.replaceState(null, "", window.location.pathname); | ||
| setIsConfirming(false); | ||
| alert("결제에 실패했거나 취소되었습니다."); | ||
| } else { | ||
| // PENDING, PROCESSING, UNKNOWN 상태일 경우 계속 폴링 | ||
| if (isPolling) { | ||
| setTimeout(pollStatus, 2000); // 2초 주기로 폴링 | ||
| } | ||
| } | ||
| } catch (error: unknown) { | ||
| console.error("결제 상태 조회 실패:", error); | ||
| isPolling = false; | ||
| window.history.replaceState(null, "", window.location.pathname); | ||
| setIsConfirming(false); | ||
| alert("결제 상태를 확인하는 중 오류가 발생했습니다."); | ||
| } | ||
| }; | ||
|
|
||
| processConfirm(); | ||
| // 동기적 setState 호출 경고(Cascading renders)를 방지하기 위해 Promise로 감싸서 실행 | ||
| Promise.resolve().then(() => { | ||
| setIsConfirming(true); | ||
| pollStatus(); | ||
| }); | ||
|
|
||
| // 폴링 중단 | ||
| return () => { | ||
| isPolling = false; | ||
| }; | ||
| } | ||
| }, [searchParams]); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
Polling is not cancellable and never terminates.
Two defects in this loop:
- The cleanup only flips
isPolling; the already-scheduledsetTimeout(Line 65) is never cleared, andpollStatusdoes not re-checkisPollingafterawait. After unmount/searchParamschange, the in-flight or scheduled call still runssetIsConfirming,alert, andwindow.location.reload(). - If the backend keeps returning
PENDING/PROCESSING, the loop polls every 2s forever, with the blocking overlay up indefinitely.
🐛 Proposed fix: cancellable, bounded polling
if (orderId) {
let isPolling = true;
+ let timer: ReturnType<typeof setTimeout> | undefined;
+ let attempts = 0;
+ const MAX_ATTEMPTS = 30; // ~60s
+
+ const finish = (message: string, reload = false) => {
+ isPolling = false;
+ window.history.replaceState(null, "", window.location.pathname);
+ setIsConfirming(false);
+ alert(message);
+ if (reload) window.location.reload();
+ };
const pollStatus = async () => {
try {
const response = await checkPaymentStatus(orderId);
+ if (!isPolling) return;
// API 타입이 지정되지 않은 경우를 대비한 타입 단언
const data = response as unknown as {
status?: string;
result?: { status?: string };
};
const status = data.status || data.result?.status;
if (status === "COMPLETED") {
- isPolling = false;
- window.history.replaceState(null, "", window.location.pathname);
- setIsConfirming(false);
- alert("크레딧 충전이 완료되었습니다!");
- window.location.reload();
+ finish("크레딧 충전이 완료되었습니다!", true);
} else if (status === "FAILED") {
- isPolling = false;
- window.history.replaceState(null, "", window.location.pathname);
- setIsConfirming(false);
- alert("결제에 실패했거나 취소되었습니다.");
+ finish("결제에 실패했거나 취소되었습니다.");
+ } else if (++attempts >= MAX_ATTEMPTS) {
+ finish("결제 결과 확인이 지연되고 있습니다. 잠시 후 다시 확인해 주세요.");
} else {
// PENDING, PROCESSING, UNKNOWN 상태일 경우 계속 폴링
- if (isPolling) {
- setTimeout(pollStatus, 2000); // 2초 주기로 폴링
- }
+ timer = setTimeout(pollStatus, 2000); // 2초 주기로 폴링
}
} catch (error: unknown) {
+ if (!isPolling) return;
console.error("결제 상태 조회 실패:", error);
- isPolling = false;
- window.history.replaceState(null, "", window.location.pathname);
- setIsConfirming(false);
- alert("결제 상태를 확인하는 중 오류가 발생했습니다.");
+ finish("결제 상태를 확인하는 중 오류가 발생했습니다.");
}
};
@@
// 폴링 중단
return () => {
isPolling = false;
+ if (timer) clearTimeout(timer);
};
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (orderId) { | |
| let isPolling = true; | |
| const pollStatus = async () => { | |
| try { | |
| await confirmPurchase(paymentKey, orderId, Number(amount)); | |
| window.history.replaceState(null, "", window.location.pathname); | |
| setIsConfirming(false); | |
| setTimeout(() => { | |
| const response = await checkPaymentStatus(orderId); | |
| // API 타입이 지정되지 않은 경우를 대비한 타입 단언 | |
| const data = response as unknown as { | |
| status?: string; | |
| result?: { status?: string }; | |
| }; | |
| const status = data.status || data.result?.status; | |
| if (status === "COMPLETED") { | |
| isPolling = false; | |
| window.history.replaceState(null, "", window.location.pathname); | |
| setIsConfirming(false); | |
| alert("크레딧 충전이 완료되었습니다!"); | |
| window.location.reload(); | |
| }, 100); | |
| } catch (error) { | |
| console.error("결제 승인 실패:", error); | |
| setIsConfirming(false); | |
| alert("결제 승인에 실패했습니다. 다시 시도해 주세요."); | |
| // 실패 시에도 쿼리 파라미터를 날려 중복 요청 방지 | |
| } else if (status === "FAILED") { | |
| isPolling = false; | |
| window.history.replaceState(null, "", window.location.pathname); | |
| setIsConfirming(false); | |
| alert("결제에 실패했거나 취소되었습니다."); | |
| } else { | |
| // PENDING, PROCESSING, UNKNOWN 상태일 경우 계속 폴링 | |
| if (isPolling) { | |
| setTimeout(pollStatus, 2000); // 2초 주기로 폴링 | |
| } | |
| } | |
| } catch (error: unknown) { | |
| console.error("결제 상태 조회 실패:", error); | |
| isPolling = false; | |
| window.history.replaceState(null, "", window.location.pathname); | |
| setIsConfirming(false); | |
| alert("결제 상태를 확인하는 중 오류가 발생했습니다."); | |
| } | |
| }; | |
| processConfirm(); | |
| // 동기적 setState 호출 경고(Cascading renders)를 방지하기 위해 Promise로 감싸서 실행 | |
| Promise.resolve().then(() => { | |
| setIsConfirming(true); | |
| pollStatus(); | |
| }); | |
| // 폴링 중단 | |
| return () => { | |
| isPolling = false; | |
| }; | |
| } | |
| }, [searchParams]); | |
| if (orderId) { | |
| let isPolling = true; | |
| let timer: ReturnType<typeof setTimeout> | undefined; | |
| let attempts = 0; | |
| const MAX_ATTEMPTS = 30; // ~60s | |
| const finish = (message: string, reload = false) => { | |
| isPolling = false; | |
| window.history.replaceState(null, "", window.location.pathname); | |
| setIsConfirming(false); | |
| alert(message); | |
| if (reload) window.location.reload(); | |
| }; | |
| const pollStatus = async () => { | |
| try { | |
| const response = await checkPaymentStatus(orderId); | |
| if (!isPolling) return; | |
| // API 타입이 지정되지 않은 경우를 대비한 타입 단언 | |
| const data = response as unknown as { | |
| status?: string; | |
| result?: { status?: string }; | |
| }; | |
| const status = data.status || data.result?.status; | |
| if (status === "COMPLETED") { | |
| finish("크레딧 충전이 완료되었습니다!", true); | |
| } else if (status === "FAILED") { | |
| finish("결제에 실패했거나 취소되었습니다."); | |
| } else if (++attempts >= MAX_ATTEMPTS) { | |
| finish("결제 결과 확인이 지연되고 있습니다. 잠시 후 다시 확인해 주세요."); | |
| } else { | |
| // PENDING, PROCESSING, UNKNOWN 상태일 경우 계속 폴링 | |
| timer = setTimeout(pollStatus, 2000); // 2초 주기로 폴링 | |
| } | |
| } catch (error: unknown) { | |
| if (!isPolling) return; | |
| console.error("결제 상태 조회 실패:", error); | |
| finish("결제 상태를 확인하는 중 오류가 발생했습니다."); | |
| } | |
| }; | |
| // 동기적 setState 호출 경고(Cascading renders)를 방지하기 위해 Promise로 감싸서 실행 | |
| Promise.resolve().then(() => { | |
| setIsConfirming(true); | |
| pollStatus(); | |
| }); | |
| // 폴링 중단 | |
| return () => { | |
| isPolling = false; | |
| if (timer) clearTimeout(timer); | |
| }; | |
| } | |
| }, [searchParams]); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@jobdri/src/app/credit/page.tsx` around lines 38 - 88, Update the polling flow
in the useEffect around pollStatus to track and clear the active timeout during
cleanup, and re-check the cancellation flag after each awaited
checkPaymentStatus call before performing state updates, alerts, history
changes, or reloads. Add a finite polling limit for PENDING, PROCESSING, and
unknown statuses; when the limit is reached, stop polling, remove the URL order
state, clear confirmation, and report that payment status could not be
confirmed. Ensure all terminal and error paths clear any scheduled timeout.
| setter: Dispatch<SetStateAction<string>>, | ||
| ) => { | ||
| setter(value); | ||
| setEmailTouched(false); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Make touched resets field-specific.
handleInputChange is shared by email, password, and password-confirm inputs, but Line 323 always clears emailTouched. Typing in password or confirmation therefore hides the email error. handlePasswordChange also resets only passwordConfirmTouched, so passwordTouched remains true while the user corrects the password.
Pass the corresponding touched setter into the shared helper, or reset each field’s touched state in its dedicated handler.
Also applies to: 333-333
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@jobdri/src/components/login/EmailLoginScreen.tsx` at line 323, Make
touched-state resets field-specific in handleInputChange and
handlePasswordChange: ensure email input changes reset only emailTouched,
password input changes reset passwordTouched, and confirmation input changes
reset passwordConfirmTouched. Pass the appropriate setter through the shared
helper or perform the resets in each dedicated handler, without clearing
unrelated fields.
| error={ | ||
| signupErrorMessage | ||
| ? signupErrorMessage | ||
| : hasSignupEmailValidationError | ||
| ? "이메일 형식이 맞지 않습니다." | ||
| : undefined | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Wire blur handling for both signup fields.
The signup email error is gated by emailTouched, but the signup email input has no onBlur, so format errors appear only after submit. The password-confirm onBlur is commented out, so mismatch errors likewise remain hidden until submit.
Add:
+ onBlur={() => setEmailTouched(true)}and restore:
- // onBlur={() => setPasswordConfirmTouched(true)}
+ onBlur={() => setPasswordConfirmTouched(true)}Also applies to: 824-824
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@jobdri/src/components/login/EmailLoginScreen.tsx` around lines 774 - 780,
Wire blur handling for both signup fields: add an onBlur handler to the signup
email input that marks the email as touched, and restore the commented
password-confirmation onBlur handler so it marks that field as touched. Update
the signup form inputs near the signupErrorMessage logic and preserve the
existing validation/error conditions.
| export async function checkPaymentStatus(orderId: string) { | ||
| const response = await fetch(`/api/payments/orders/${orderId}`, { | ||
| method: "GET", | ||
| }); | ||
| checkResponse(response, "결제 승인에 실패했습니다."); | ||
| if (!response.ok) throw new Error("Failed to fetch payment status"); | ||
| return response.json(); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Both callers use as unknown as casts because the credit API layer doesn't declare its response shapes. Fix the contracts at the source instead of asserting at each call site.
jobdri/src/lib/api/credit.ts#L124-L129: givecheckPaymentStatusan explicit return type and unwrapresult; addcheckoutPagetoPreparePaymentResult.jobdri/src/components/common/cards/CreditCard.tsx#L57-L72: drop the cast and the unreachabledata.result?.checkoutPagefallback, readingresponse.checkoutPage.jobdri/src/app/credit/page.tsx#L45-L49: drop the cast and readresponse.statusdirectly.
📍 Affects 3 files
jobdri/src/lib/api/credit.ts#L124-L129(this comment)jobdri/src/components/common/cards/CreditCard.tsx#L57-L72jobdri/src/app/credit/page.tsx#L45-L49
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@jobdri/src/lib/api/credit.ts` around lines 124 - 129, Update
jobdri/src/lib/api/credit.ts lines 124-129 by giving checkPaymentStatus an
explicit response type and returning the unwrapped result; extend
PreparePaymentResult with checkoutPage. In
jobdri/src/components/common/cards/CreditCard.tsx lines 57-72, remove the cast
and fallback, and read response.checkoutPage directly. In
jobdri/src/app/credit/page.tsx lines 45-49, remove the cast and read
response.status directly.
🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win
checkPaymentStatus breaks the conventions every other call in this file follows: missing API_BASE_URL, auth headers, and checkResponse.
The relative path resolves against the Next.js app origin instead of the API host, and without getAuthHeaders() a user-scoped order lookup will 401. Bypassing checkResponse also skips handleUnauthorized() on 401, so the polling loop in jobdri/src/app/credit/page.tsx (Lines 68-74) surfaces a generic Korean alert instead of redirecting to login. Also unwrap result and type the return so callers don't need as unknown as assertions.
🔧 Proposed fix
-export async function checkPaymentStatus(orderId: string) {
- const response = await fetch(`/api/payments/orders/${orderId}`, {
- method: "GET",
- });
- if (!response.ok) throw new Error("Failed to fetch payment status");
- return response.json();
-}
+export type PaymentStatus =
+ | "PENDING"
+ | "PROCESSING"
+ | "COMPLETED"
+ | "FAILED";
+
+export async function checkPaymentStatus(
+ orderId: string,
+): Promise<{ status: PaymentStatus }> {
+ const response = await fetch(
+ `${API_BASE_URL}/api/payments/orders/${encodeURIComponent(orderId)}`,
+ {
+ method: "GET",
+ headers: getAuthHeaders(),
+ },
+ );
+ checkResponse(response, "결제 상태 조회에 실패했습니다.");
+ const { result }: ApiResponse<{ status: PaymentStatus }> =
+ await response.json();
+ return result;
+}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export async function checkPaymentStatus(orderId: string) { | |
| const response = await fetch(`/api/payments/orders/${orderId}`, { | |
| method: "GET", | |
| }); | |
| checkResponse(response, "결제 승인에 실패했습니다."); | |
| if (!response.ok) throw new Error("Failed to fetch payment status"); | |
| return response.json(); | |
| export type PaymentStatus = | |
| | "PENDING" | |
| | "PROCESSING" | |
| | "COMPLETED" | |
| | "FAILED"; | |
| export async function checkPaymentStatus( | |
| orderId: string, | |
| ): Promise<{ status: PaymentStatus }> { | |
| const response = await fetch( | |
| `${API_BASE_URL}/api/payments/orders/${encodeURIComponent(orderId)}`, | |
| { | |
| method: "GET", | |
| headers: getAuthHeaders(), | |
| }, | |
| ); | |
| checkResponse(response, "결제 상태 조회에 실패했습니다."); | |
| const { result }: ApiResponse<{ status: PaymentStatus }> = | |
| await response.json(); | |
| return result; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@jobdri/src/lib/api/credit.ts` around lines 124 - 129, Update
checkPaymentStatus to follow the file’s established API request pattern: build
the URL with API_BASE_URL, include getAuthHeaders(), and pass the response
through checkResponse so unauthorized responses invoke handleUnauthorized().
Unwrap the response’s result field and declare the returned payment-status type,
allowing callers to use it without casts.
🔗 관련 이슈
📝 개요
⌨️ 작업 상세 내용
유효성 검사 타이밍 변경 (Submit-driven Validation)
emailTouched,passwordTouched,passwordConfirmTouched상태를 추가하여 에러 노출 여부를 제어합니다.InputMain에서 사용 중이던 불필요한 이벤트(onBlur등)를 제거했습니다.handleSignupSubmit함수(회원가입 버튼 클릭) 내에서touched상태들을 모두true로 변경하여 제출 시점에만 정규식 및 일치 여부 에러가 표시되도록 수정했습니다.재입력 시 에러 메시지 숨김 처리 (UX 개선)
사용자가 에러를 확인한 후 다시 입력을 시작하면(
onChange발생 시) 해당 입력칸의touched상태를false로 돌려, 다시 제출 버튼을 누르기 전까지 에러 메시지를 숨겨줍니다.handleInputChange,handlePasswordChange,handlePasswordConfirmChange함수에 관련 상태 초기화 로직을 추가했습니다.InputMain컴포넌트 Error Prop 조건부 렌더링 수정hasSignupEmailValidationError등)을 분리하고, 에러가 없을 때는undefined를 전달하여 UI에서 에러가 깔끔하게 사라지도록 조치했습니다.📸 스크린샷 (UI 변경 시)
JobDri.-.Chrome.2026-07-30.13-03-10.mp4
🔍 리뷰 요구사항 (Reviewers)
Summary by CodeRabbit
New Features
Bug Fixes